import type { ParseResult, TimeslotInfo } from '@shared/types/'
import type { HttpResponse, HttpSession, IHttpSession } from './http-session'
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { parseDDMMYYYY } from '../booking/helpers/calendar'
import { generateRandomRelease } from '../booking/helpers/mock-release'
import { getConfig, getRequestHtmlPath } from '../config/index'
import { getLogger } from '../logger/index'
import { UserLogger } from '../logger/user-logger'
import { getPageParser } from '../shared/page-parser'

/**
 * 更新偏好设置
 */
export interface UpdatePreferences {
  officeCode?: string
  preferredDates?: string[]
  preferredTimes?: string[]
  /** 是否强制使用偏好日期（如果设置为true，只有命中偏好日期才会预约，否则失败） */
  forcePreferredDates?: boolean
  /** 会话级允许日期范围（覆盖配置） */
  allowedDateRange?: { start?: string, end?: string }
  /** Job ID（可选）：用于关联UI日志记录 */
  jobId?: string
  /** 进度回调（可选）：用于上报细粒度进度与步骤 */
  onProgress?: (p: { progress: number, step: string, timestamp: number }) => void
}

/**
 * 预约数据信息（用于目录分类）
 */
export interface AppointmentMetadata {
  appointmentId?: string | number
  passportNumber?: string
  licenseNumber?: string
  fullName?: string
  sequenceNumber?: string
  /** 标记此快照来源于“监控探测”而非真实预约 */
  isMonitor?: boolean
}

/**
 * TDABS（香港政府网上预约系统）流程处理类
 */
export class TdabsFlows {
  private session: HttpSession
  private baseUrl: string
  private parser = getPageParser()
  private logger = getLogger()
  /** Job ID（用于UI日志记录） */
  private jobId: string | undefined
  /** 进度回调 */
  private onProgress?: (p: { progress: number, step: string, timestamp: number }) => void
  /** 预约数据信息（用于HTML分类存储） */
  private appointmentMetadata: AppointmentMetadata | undefined
  /** 当前运行的目录（用于HTML存储） */
  private currentRunDir: string | null = null

  /**
   * 获取当前运行的目录路径（按时间和预约信息分类）
   */
  private async getCurrentRunDir(): Promise<string> {
    if (this.currentRunDir)
      return this.currentRunDir

    try {
      const cfg = getConfig()
      if (!cfg.logging.save_html)
        return ''

      const baseDir = getRequestHtmlPath()
      await mkdir(baseDir, { recursive: true })

      // 使用中国时区时间戳
      const now = new Date()
      const utcMs = now.getTime() + now.getTimezoneOffset() * 60000
      const chinaMs = utcMs + 8 * 60 * 60000
      const d = new Date(chinaMs)
      const y = d.getFullYear()
      const m = String(d.getMonth() + 1).padStart(2, '0')
      const day = String(d.getDate()).padStart(2, '0')
      const hh = String(d.getHours()).padStart(2, '0')
      const mm = String(d.getMinutes()).padStart(2, '0')
      const ss = String(d.getSeconds()).padStart(2, '0')
      const ts = `${y}-${m}-${day}_${hh}-${mm}-${ss}`

      // 构建目录名：时间戳 + 预约信息
      let dirName = ts
      if (this.appointmentMetadata) {
        const meta = this.appointmentMetadata
        const parts: string[] = []

        // 预约ID（优先使用sequence_number）
        if (meta.sequenceNumber) {
          parts.push(meta.sequenceNumber)
        }
        else if (meta.appointmentId) {
          parts.push(String(meta.appointmentId))
        }

        // 护照后4位（用于标识）
        if (meta.passportNumber) {
          const passportTail = meta.passportNumber.slice(-4).toUpperCase()
          parts.push(passportTail)
        }

        // 姓名（安全化处理，限制长度）
        if (meta.fullName) {
          const safeName = meta.fullName.replace(/[/:*?"<>|]/g, '_').trim().slice(0, 20)
          if (safeName)
            parts.push(safeName)
        }

        if (parts.length > 0)
          dirName = `${ts}_${parts.join('_')}`
      }

      const fullPath = join(baseDir, dirName)
      await mkdir(fullPath, { recursive: true })
      this.currentRunDir = fullPath

      // 保存预约元数据到目录中（便于后续排查）
      if (this.appointmentMetadata) {
        try {
          const metadataFile = join(fullPath, 'appointment-metadata.json')
          await writeFile(metadataFile, JSON.stringify({
            appointmentId: this.appointmentMetadata.appointmentId,
            sequenceNumber: this.appointmentMetadata.sequenceNumber,
            passportNumber: this.appointmentMetadata.passportNumber ? `${this.appointmentMetadata.passportNumber.slice(0, 4)}****${this.appointmentMetadata.passportNumber.slice(-4)}` : undefined,
            licenseNumber: this.appointmentMetadata.licenseNumber ? `${this.appointmentMetadata.licenseNumber.slice(0, 2)}****${this.appointmentMetadata.licenseNumber.slice(-4)}` : undefined,
            fullName: this.appointmentMetadata.fullName,
            isMonitor: this.appointmentMetadata.isMonitor === true,
            createdAt: new Date().toISOString(),
          }, null, 2), 'utf8')
        }
        catch {
          // 忽略元数据文件写入失败
        }
      }

      return fullPath
    }
    catch {
      // 出错时返回基础目录
      return getRequestHtmlPath()
    }
  }

  /**
   * 保存HTML到文件
   * @param name 文件名
   * @param html HTML内容
   * @returns 保存的文件路径
   */
  private async saveHtml(name: string, html: string): Promise<string | undefined> {
    try {
      const cfg = getConfig()
      if (!cfg.logging.save_html)
        return undefined

      // 使用分类目录
      const dir = await this.getCurrentRunDir()
      if (!dir)
        return undefined

      // 文件名仅包含步骤名称和时间戳（秒级，避免文件名过长）
      const now = new Date()
      const ts = `${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}`
      const file = join(dir, `${ts}_${name}.html`)
      await writeFile(file, html, 'utf8')
      return file
    }
    catch { return undefined }
  }

  /**
   * 保存请求信息到文件（用于分析）
   */
  private async saveRequestInfo(name: string, content: string): Promise<string | undefined> {
    try {
      const cfg = getConfig()
      if (!cfg.logging.save_html)
        return undefined

      // 使用分类目录
      const dir = await this.getCurrentRunDir()
      if (!dir)
        return undefined

      // 文件名仅包含步骤名称和时间戳（秒级）
      const now = new Date()
      const ts = `${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}`
      const file = join(dir, `${ts}_${name}.json`)
      await writeFile(file, content, 'utf8')
      return file
    }
    catch { return undefined }
  }

  constructor(session: IHttpSession, appointmentMetadata?: AppointmentMetadata) {
    const cfg = getConfig()
    this.session = session
    this.baseUrl = cfg.network.base_url
    this.appointmentMetadata = appointmentMetadata
  }

  /**
   * 设置预约数据信息（用于HTML分类存储）
   */
  setAppointmentMetadata(metadata: AppointmentMetadata): void {
    this.appointmentMetadata = metadata
    // 重置目录，下次保存时会使用新的元数据创建目录
    this.currentRunDir = null
  }

  /**
   * 记录进度（UI日志）
   */
  private async logProgress(progress: number, step: string, message?: string): Promise<void> {
    // 调用 UserLogger
    if (this.jobId) {
      await UserLogger.step(this.jobId, step, progress, message)
    }

    // 调用 onProgress 回调（如果存在）
    if (this.onProgress) {
      this.onProgress({
        progress,
        step: message || step,
        timestamp: Math.floor(Date.now() / 1000),
      })
    }
  }

  /**
   * 记录错误（UI日志）
   */
  private async logError(error: Error | string, context?: string): Promise<void> {
    if (this.jobId) {
      await UserLogger.error(this.jobId, error, context)
    }
  }

  /**
   * 构造URL（cmd开头时自动添加问号）
   */
  private url(cmd: string): string {
    if (cmd.startsWith('?'))
      return `${this.baseUrl}${cmd}`
    return `${this.baseUrl}?${cmd}`
  }

  /**
   * 基础表单（lang: tchinese, timestamp）
   */
  private baseForm(extra?: Record<string, string>): Record<string, string> {
    return {
      lang: 'tchinese',
      timestamp: Date.now().toString(),
      ...(extra || {}),
    }
  }

  /**
   * 解析日历统计信息（从HTML字符串）
   * 返回年月、总天数、可预约天数、休息日数、约满天数和暂不可约天数
   */
  private analyzeCalendarSummary(html: string): { year: number, monthIndex0: number, daysInMonth: number, availableCount: number, restCount: number, bookedCount: number, tempCount: number } | null {
    try {
      const res = this.parser.parseCalendarSummary(html)
      if (res.success && res.data)
        return res.data
      return null
    }
    catch (error) {
      console.log('[TdabsFlows] analyzeCalendarSummary 解析失败:', error instanceof Error ? error.message : String(error))
      return null
    }
  }

  /**
   * 开始新操作（cmdStartNewApplication）
   */
  async startOperation(operation: 'E' | 'U'): Promise<{ html: string, url: string, html_saved?: string }> {
    console.log('[TdabsFlows] startOperation', operation)
    const res = await this.session.postForm(
      this.url('cmd=cmdStartNewApplication'),
      this.baseForm({ storedAppointmentOperation: operation }),
      this.url('cmd=cmdShowHome'),
    )
    const saved = await this.saveHtml(`request_${operation}_01_start`, res.body)
    return { html: res.body, url: res.url, html_saved: saved }
  }

  /**
   * 进入入口页（cmdEnquireAppointment 或 cmdUpdateAppointment）
   */
  async enterEntry(operation: 'E' | 'U'): Promise<{ html: string, url: string, html_saved?: string }> {
    console.log('[TdabsFlows] enterEntry', operation)
    const cmd = operation === 'E' ? 'cmd=cmdEnquireAppointment' : 'cmd=cmdUpdateAppointment'
    const res = await this.session.postForm(
      this.url(cmd),
      this.baseForm({ storedAppointmentOperation: operation }),
      this.url('cmd=cmdStartNewApplication'),
    )
    const saved = await this.saveHtml(`request_${operation}_02_entry`, res.body)
    return { html: res.body, url: res.url, html_saved: saved }
  }

  /**
   * 按参考号获取预约详情（入口页 → cmdGetApptInfoByRefNo）
   */
  async getApptInfoByRef(entryHtml: string, refLast6: string, operation: 'E' | 'U', _serviceType?: 'DI' | 'DL' | 'VRM' | 'IDP'): Promise<{ html: string, url: string, html_saved?: string }> {
    console.log('[TdabsFlows] getApptInfoByRef', { refLast6, operation })

    // 从 entry 页面提取隐藏字段，携带服务端生成的 token / session 字段
    const entryFields = this.parser.extractFormFields(entryHtml, 'frmAppointment')
    const payload: Record<string, string> = {
      ...this.baseForm({
        storedAppointmentOperation: operation,
        inputType: 'N',
        storedAppointmentRequestChannel: 'W',
      }),
      ...(entryFields.success && entryFields.data ? entryFields.data : {}),
    }
    // 统一设置查询关键参数
    payload.storedAppointmentReferenceNo = refLast6
    // 固定 DI 类型（按文档要求）
    payload.storedAppointmentServiceType = 'DI'

    const res = await this.session.postForm(
      this.url('cmd=cmdGetApptInfoByRefNo'),
      payload,
      this.url(operation === 'E' ? 'cmd=cmdEnquireAppointment' : 'cmd=cmdUpdateAppointment'),
    )
    const saved = await this.saveHtml(`request_${operation}_03_getInfo_${refLast6}`, res.body)
    return { html: res.body, url: res.url, html_saved: saved }
  }

  /**
   * 按证件/执照获取预约详情（入口页 → cmdCheckIdentifierAction）
   *
   * 字段映射规则：
   * - HKID: storedAppointmentLicenseType = 'H'; storedAppointmentLicenseNumber = hkid
   * - 护照: storedAppointmentLicenseType = 'P'; storedAppointmentLicenseNumber = passport
   * - 驾照: storedAppointmentLicenseNumber2 = license（可选）
   * 其他：统一固定服务类型为 DI，合并入口页隐藏字段以带上 token/timestamp 等。
   */
  async getApptInfoByIdentity(entryHtml: string, id: { passport?: string, hkid?: string, license?: string }): Promise<{ html: string, url: string, html_saved?: string }> {
    console.log('[TdabsFlows] getApptInfoByIdentity', { hasPassport: !!id.passport, hasHkid: !!id.hkid, hasLicense: !!id.license })

    const entryFields = this.parser.extractFormFields(entryHtml, 'frmAppointment')
    const base = (entryFields.success && entryFields.data) ? entryFields.data : {}

    // 推断证件类型与字段，参考页面脚本：
    // - storedAppointmentLicenseType: 'P'(护照) | 'O'(身份证/其他) | 可能还有其他分支
    // - storedAppointmentLicenseNumber: 主证件号
    // - storedAppointmentLicenseNumber2: 驾照号（可选）
    const payload: Record<string, string> = {
      ...base,
      ...this.baseForm({
        storedAppointmentOperation: 'E',
        inputType: 'N',
        storedAppointmentRequestChannel: 'W',
        storedAppointmentServiceType: 'DI',
        userAgent: '',
      }),
    }

    // 证件类型映射（对齐页面 onClickContinue 的 DI 分支）：
    // - DI 分支无论身份证/护照，均使用 storedAppointmentLicenseType = 'P'
    // - 主证件号 = 护照/香港身份证其一（去空格）；驾照号填入 storedAppointmentLicenseNumber2
    const idMain = (id.passport || id.hkid || '').toString().trim()
    if (idMain) {
      payload.storedAppointmentLicenseType = 'P'
      payload.storedAppointmentLicenseNumber = idMain
      if (id.license)
        payload.storedAppointmentLicenseNumber2 = id.license
    }
    else if (id.license) {
      // 兜底：仅提供了驾照号（理论上不会发生，因为上层已校验）
      payload.storedAppointmentLicenseType = 'O'
      payload.storedAppointmentLicenseNumber = id.license
    }

    // 其他与页面一致的固定字段（避免校验不通过）：
    // - 过期日为空场景统一设为 11-11-1901（站点常用哨兵值）
    // - 电话占位为 N/A（与页面行为一致）
    if (!payload.storedAppointmentExpiryDate)
      payload.storedAppointmentExpiryDate = '11-11-1901'
    if (!payload.storedAppointmentPhoneNumber)
      payload.storedAppointmentPhoneNumber = 'N/A'

    // 载荷概要（去敏）
    try {
      const mask = (s?: string) => {
        if (!s)
          return ''
        const raw = String(s)
        if (raw.length <= 4)
          return `${raw[0] || ''}***${raw.slice(-1)}`
        return `${raw.slice(0, 1)}***${raw.slice(-2)}`
      }
      this.logger.info('【TDABS】byId 请求载荷概要', {
        licenseType: payload.storedAppointmentLicenseType,
        idMasked: mask(payload.storedAppointmentLicenseNumber),
        licenseMasked: mask(payload.storedAppointmentLicenseNumber2),
        baseFieldCount: Object.keys(base).length,
      })
    }
    catch {}

    const res = await this.session.postForm(
      this.url('cmd=cmdCheckIdentifierAction'),
      payload,
      this.url('cmd=cmdEnquireAppointment'),
    )
    const saved = await this.saveHtml('request_E_03_getInfo_byId', res.body)
    try {
      const pageType = this.parser.identifyPageType(res.body, res.url)
      this.logger.info('【TDABS】byId 返回页概要', { url: res.url, pageType, saved })
    }
    catch {}
    return { html: res.body, url: res.url, html_saved: saved }
  }

  /**
   * 从预约详情页取消预约（cmdCancelAppointmentAction）
   */
  async cancelFromApptInfo(apptHtml: string): Promise<{ html: string, success: boolean, message?: string }> {
    const fields = this.parser.extractFormFields(apptHtml, 'frmAppointment')
    if (!fields.success) {
      console.warn('[TdabsFlows] extractFormFields failed on cancel, using base form only')
    }

    // 从页面解析出完整参考编号，作为 referenceNumber 传入
    const brief = this.parser.parseAppointmentInfo(apptHtml)
    const fullRef = brief.success ? (brief.data as any)?.reference_number : undefined

    // 注意：必须保留页面里的 token（如 timestamp 等），因此让页面字段覆盖基础字段
    const form: Record<string, string> = {
      ...this.baseForm({
        storedAppointmentOperation: 'U',
        inputType: 'N',
        storedAppointmentRequestChannel: 'W',
        storedAppointmentServiceType: 'DI',
      }),
      ...(fields.success && fields.data ? fields.data : {}),
    }
    if (fullRef) {
      form.referenceNumber = fullRef
      form.storedAppointmentReferenceNo = fullRef
    }
    // 按文档：保留页面原有的所有隐藏字段（包括 timeslot/证件/邮箱/电话等），避免缺失造成失败

    console.log('[TdabsFlows] cancel form keys', Object.keys(form).slice(0, 12), '...')
    // 取消动作：Referer 指向上一步（get info页）更稳定
    const res = await this.session.postForm(this.url('cmd=cmdCancelAppointmentAction'), form, this.url('cmd=cmdGetApptInfoByRefNo'))
    const html = res.body
    await this.saveHtml('request_U_04_cancel_result', html)
    // 使用纯文本判定，容忍中间有标签/空白
    try {
      const cheerio = await import('cheerio')
      const $ = cheerio.load(html)
      const bodyText = $('body').text().replace(/\s+/g, '')
      const success = bodyText.includes('你的預約已經取消') || bodyText.includes('你的预约已经取消') || bodyText.includes('取消预约成功') || bodyText.includes('取消預約成功')
      let msg: string | undefined
      if (success) {
        const m = bodyText.match(/(參考編號|参考编号)[:：]?([A-Z0-9\-]+)/i)
        msg = m ? `取消成功: ${m[2]}` : '取消成功'
      }
      return { html, success, message: msg }
    }
    catch {
      // 回退：宽松正则，允许中间穿插标签
      const success = /你的[\s\S]{0,20}預約[\s\S]{0,20}已經[\s\S]{0,20}取消|你的[\s\S]{0,20}预约[\s\S]{0,20}已经[\s\S]{0,20}取消/.test(html)
      let msg: string | undefined
      if (success) {
        const match = html.replace(/<[^>]+>/g, ' ').match(/(參考編號|参考编号)[：:\s]*([A-Z0-9\s-]+)/i)
        msg = match ? `取消成功: ${match[2].replace(/\s+/g, '')}` : '取消成功'
      }
      return { html, success, message: msg }
    }
  }

  /**
   * 从预约详情页更新时段（cmdGetCalendarAction → cmdGetTimeslotListAction → cmdConfirmAppointment → cmdSubmitAppointmentAction）
   */
  async updateTimeslotFromApptInfo(apptHtml: string, prefs: UpdatePreferences): Promise<{ html: string, success: boolean, reference?: string, selectedDate?: string, selectedTimeslot?: string, error?: string }> {
    const baseFields = this.parser.extractFormFields(apptHtml, 'frmAppointment')
    if (!baseFields.success) {
      console.warn('[TdabsFlows] extractFormFields failed on update (step0)')
    }
    const form0 = {
      ...(baseFields.success && baseFields.data ? baseFields.data : {}),
      ...this.baseForm({ storedAppointmentOperation: 'U' }),
    }

    // 1) 选择办事处
    const officeCode = prefs.officeCode || 'HKLO'
    const resOffice = await this.session.postForm(this.url('cmd=cmdUpdateAppointmentSelectOffice'), { ...form0, storedAppointmentOfficeCode: officeCode }, this.url('cmd=cmdGetApptInfoByRefNo'))

    // 2) 获取日历
    const form1Fields = this.parser.extractFormFields(resOffice.body, 'frmAppointment')
    if (!form1Fields.success)
      console.warn('[TdabsFlows] extractFormFields failed (calendar)')
    const form1 = { ...(form1Fields.success && form1Fields.data ? form1Fields.data : {}), ...this.baseForm({ storedAppointmentOperation: 'U' }) }
    const resCalendar = await this.session.postForm(this.url('cmd=cmdGetCalendarAction'), form1, this.url('cmd=cmdUpdateAppointmentSelectOffice'))

    const datesResult: ParseResult<string[]> = this.parser.parseAvailableDates(resCalendar.body)
    if (!datesResult.success || !datesResult.data || datesResult.data.length === 0)
      return { html: resCalendar.body, success: false, error: '无可用日期' }

    // 允许日期范围（未命中首选时使用）
    // 使用会话传入的 allowedDateRange（来自 UI）；全局不再提供该配置
    const allowed = (prefs.allowedDateRange && (prefs.allowedDateRange.start || prefs.allowedDateRange.end))
      ? prefs.allowedDateRange
      : undefined
    const parseAllowed = (s?: string): Date | undefined => {
      if (!s || !String(s).trim())
        return undefined
      const v = String(s).trim()
      if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
        const [y, m, d] = v.split('-').map(Number)
        const dt = new Date(y, m - 1, d); dt.setHours(0, 0, 0, 0)
        return dt
      }
      try {
        const [dd, mm, yyyy] = v.split('-').map(Number)
        const dt = new Date(yyyy, (mm || 1) - 1, dd)
        dt.setHours(0, 0, 0, 0)
        return dt
      }
      catch { return undefined }
    }
    const allowedStart = parseAllowed(allowed?.start)
    const allowedEnd = parseAllowed(allowed?.end)

    let selectedDate = datesResult.data[0]
    // 首选优先（不受 allowed 限制）—兼容 YYYY-MM-DD 与 DD-MM-YYYY
    const toDMY = (v: string) => {
      const s = String(v || '').trim()
      if (/^\d{2}-\d{2}-\d{4}$/.test(s))
        return s
      const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
      if (m)
        return `${m[3]}-${m[2]}-${m[1]}`
      return s
    }
    if (prefs.preferredDates && prefs.preferredDates.length > 0) {
      const prefDMY = prefs.preferredDates.map(toDMY)
      const hit = prefDMY.find(d => datesResult.data!.includes(d))
      if (hit) {
        selectedDate = hit
      }
    }
    // 若未命中首选，应用 allowed 过滤
    if (!prefs.preferredDates || !prefs.preferredDates.includes(selectedDate)) {
      const toDate = (dmy: string) => {
        const [dd, mm, yyyy] = dmy.split('-')
        const dt = new Date(Number(yyyy), Number(mm) - 1, Number(dd))
        dt.setHours(0, 0, 0, 0)
        return dt
      }
      const inAllowed = (dmy: string) => {
        const dt = toDate(dmy)
        if (allowedStart && dt < allowedStart)
          return false
        if (allowedEnd && dt > allowedEnd)
          return false
        return true
      }
      const candidates = datesResult.data.filter(inAllowed)
      if (candidates.length > 0) {
        selectedDate = candidates[0]
      }
      else if (allowedStart || allowedEnd) {
        // 当用户限定了允许日期范围且没有任何可预约日期命中范围时，严格遵守范围：直接返回错误，避免越界预约
        return { html: resCalendar.body, success: false, error: '无可用日期在允许范围内' }
      }
    }

    // 3) 获取时段
    const form2Fields = this.parser.extractFormFields(resCalendar.body, 'frmAppointment')
    const form2 = { ...(form2Fields.success && form2Fields.data ? form2Fields.data : {}), ...this.baseForm({ storedAppointmentOperation: 'U', storedAppointmentTimeslotDate: selectedDate }) }
    const resTimeslot = await this.session.postForm(this.url('cmd=cmdGetTimeslotListAction'), form2, this.url('cmd=cmdGetCalendarAction'))

    const tResult = this.parser.parseAvailableTimeslots(resTimeslot.body)
    if (!tResult.success || !tResult.data || tResult.data.length === 0)
      return { html: resTimeslot.body, success: false, error: '无可用时段' }

    let selected: TimeslotInfo = tResult.data[0]
    if (prefs.preferredTimes && prefs.preferredTimes.length > 0) {
      for (const p of prefs.preferredTimes) {
        const found = tResult.data.find(s => s.display_time.includes(p) || s.start_time === p.replace(':', ''))
        if (found) {
          selected = found
          break
        }
      }
    }

    // 4) 确认预约
    const form3Fields = this.parser.extractFormFields(resTimeslot.body, 'frmAppointment')
    const form3 = {
      ...(form3Fields.success && form3Fields.data ? form3Fields.data : {}),
      ...this.baseForm({
        storedAppointmentOperation: 'U',
        storedAppointmentTimeslotId: selected.id,
        storedAppointmentTimeslotFromTime: selected.start_time,
        storedAppointmentTimeslotToTime: selected.end_time,
      }),
    }
    const resConfirm = await this.session.postForm(this.url('cmd=cmdConfirmAppointment'), form3, this.url('cmd=cmdGetTimeslotListAction'))

    // 5) 提交
    const finalFields = this.parser.extractFormFields(resConfirm.body, 'frmAppointment')
    const form4 = { ...(finalFields.success && finalFields.data ? finalFields.data : {}), ...this.baseForm({ storedAppointmentOperation: 'U' }) }
    const resSubmit = await this.session.postForm(this.url('cmd=cmdSubmitAppointmentAction'), form4, this.url('cmd=cmdConfirmAppointment'))

    // 解析结果
    const booking = this.parser.parseBookingResult(resSubmit.body)
    if (booking.success && booking.data?.success) {
      const ref = booking.data.reference_number
      return {
        html: resSubmit.body,
        success: true,
        reference: ref,
        selectedDate: selectedDate || undefined, // 实际选择的日期
        selectedTimeslot: selected?.display_time || undefined, // 实际选择的时段
      }
    }
    return { html: resSubmit.body, success: false, error: booking.error || booking.data?.message || '提交失败' }
  }

  /**
   * 访问首页（cmdShowHome）
   */
  async navigateHome(): Promise<{ html: string, url: string, html_saved?: string }> {
    console.log('[TdabsFlows] navigateHome 访问首页')
    const res = await this.session.request({
      method: 'GET',
      url: this.url('cmd=cmdShowHome'),
    })
    const saved = await this.saveHtml('booking_01_home', res.body)
    return { html: res.body, url: res.url, html_saved: saved }
  }

  /**
   * 同意条款并进入个人信息页（cmdStartNewApplication → cmdInputPersonalInfo）
   * 流程：
   * 1. 从首页点击"开始预约" -> storedAppointmentOperation='A' -> 提交到 cmdStartNewApplication（条款页）
   * 2. 在条款页勾选checkbox -> 调用 onClickStartApplication() -> 提交到 cmdInputPersonalInfo（个人信息页）
   */
  async agreeTermsAndEnterPersonalInfo(): Promise<{ html: string, url: string, html_saved?: string }> {
    console.log('[TdabsFlows] agreeTermsAndEnterPersonalInfo 同意条款')
    // 第一步：从首页点击"开始预约"进入条款页
    // 关键：必须设置 storedAppointmentOperation = 'A'（新建预约），与首页 onClickStartApplication() 逻辑一致
    const res1 = await this.session.postForm(
      this.url('cmd=cmdStartNewApplication'),
      this.baseForm({
        storedAppointmentOperation: 'A', // 新建预约（A = 开始新预约）
      }),
      this.url('cmd=cmdShowHome'),
    )
    await this.saveHtml('booking_02_terms', res1.body)

    // 第二步：在条款页勾选同意并提交进入个人信息页
    // 从条款页HTML：onClickStartApplication() 设置 action = cmdInputPersonalInfo，doOutput() 检查checkbox
    const formFields = this.parser.extractFormFields(res1.body, 'frmAppointment')
    const form = {
      ...(formFields.success && formFields.data ? formFields.data : {}),
      ...this.baseForm({
        storedAppointmentOperation: 'A', // 继续新建预约流程
        // checkbox 通过 name="checkbox" 字段传递，但通常服务端通过 agree 或直接检查表单状态判断
        // 实际上，从HTML看，doOutput() 只是检查checkbox是否选中，然后调用onClickStartApplication()
        // onClickStartApplication() 设置action并提交，不需要额外的字段
      }),
    }

    // 提交到个人信息页（action已在HTML中设置为cmdInputPersonalInfo）
    const res2 = await this.session.postForm(
      this.url('cmd=cmdInputPersonalInfo'),
      form,
      this.url('cmd=cmdStartNewApplication'),
    )
    const saved2 = await this.saveHtml('booking_03_personal_info', res2.body)
    return { html: res2.body, url: res2.url, html_saved: saved2 }
  }

  /**
   * 填写个人信息并提交身份验证（个人信息页 → 身份验证）
   * 流程：在个人信息页选择DI服务、填写信息后，调用 onClickContinue() 提交到 cmdCheckIdentifierAction
   *
   * 根据HTML源码中的onClickContinue()函数（DI分支）：
   * - storedAppointmentServiceType = 'DI'
   * - storedAppointmentLicenseType = 'P'
   * - storedAppointmentLicenseNumber = diPassportNumber (护照号)
   * - storedAppointmentLicenseNumber2 = txtLicenseNumber (驾照号)
   * - storedAppointmentExpiryDate = '11-11-1901'
   * - storedAppointmentRegMarkNumber = ''
   * - storedAppointmentShortRegMarkNumber = ''
   * - storedAppointmentIssuingCountry = 选中的issuingCountry
   * - storedAppointmentLicenseOwnerName = txtLicenseOwnerName
   * - storedAppointmentIssuingAuthority = 选中的issuingAuthority
   * - storedAppointmentReminderEmail = txtEmailAddress
   * - storedAppointmentConfirmationEmail = txtEmailAddress
   * - storedAppointmentPhoneNumber = 'N/A'
   * - transferOfOwnership = 'N'
   * - sendReminder = 'Y'
   * - inputType = 'N'
   * - action = cmdCheckIdentifierAction
   */
  async submitPersonalInfoAndVerifyIdentity(
    personalInfoHtml: string,
    id: { passport?: string, hkid?: string, license?: string },
    userInfo: { name?: string, email?: string, phone?: string, issuingCountry?: string, issuingAuthority?: string },
  ): Promise<{ html: string, url: string, html_saved?: string }> {
    console.log('[TdabsFlows] submitPersonalInfoAndVerifyIdentity 提交身份验证')

    // 从个人信息页提取所有隐藏字段，保持会话状态
    const formFields = this.parser.extractFormFields(personalInfoHtml, 'frmAppointment')
    const base = (formFields.success && formFields.data) ? formFields.data : {}

    // 构建提交表单（完全对齐页面 onClickContinue() 中 DI 分支的逻辑）
    const payload: Record<string, string> = {
      ...base, // 保留所有原有隐藏字段
      ...this.baseForm({
        storedAppointmentOperation: 'A', // 继续新建预约流程
        storedAppointmentServiceType: 'DI', // DI服务
        storedAppointmentLicenseType: 'P', // 护照
        inputType: 'N', // 新建
        storedAppointmentRequestChannel: 'W', // Web渠道
        storedAppointmentExpiryDate: '11-11-1901', // DI服务固定值
        storedAppointmentRegMarkNumber: '',
        storedAppointmentShortRegMarkNumber: '',
        transferOfOwnership: 'N',
        sendReminder: 'Y',
        storedAppointmentPhoneNumber: 'N/A', // onClickContinue()中设置为'N/A'
      }),
    }

    // 填充身份信息（对齐 onClickContinue() DI分支）
    if (id.passport) {
      payload.storedAppointmentLicenseNumber = id.passport // diPassportNumber
    }
    if (id.license) {
      payload.storedAppointmentLicenseNumber2 = id.license // txtLicenseNumber
    }
    if (userInfo.name) {
      payload.storedAppointmentLicenseOwnerName = userInfo.name // txtLicenseOwnerName
    }
    if (userInfo.email) {
      payload.storedAppointmentReminderEmail = userInfo.email // txtEmailAddress
      payload.storedAppointmentConfirmationEmail = userInfo.email // txtEmailAddress
    }
    if (userInfo.issuingCountry) {
      payload.storedAppointmentIssuingCountry = userInfo.issuingCountry
    }
    if (userInfo.issuingAuthority) {
      payload.storedAppointmentIssuingAuthority = userInfo.issuingAuthority
    }

    // 提交到身份验证接口（onClickContinue() 设置的action）
    const res = await this.session.postForm(
      this.url('cmd=cmdCheckIdentifierAction'),
      payload,
      this.url('cmd=cmdInputPersonalInfo'),
    )
    const saved = await this.saveHtml('booking_04_identity_verify', res.body)
    return { html: res.body, url: res.url, html_saved: saved }
  }

  /**
   * 新建预约（基于身份信息），完整流程：首页 → 同意条款 → 个人信息 → 身份验证 → 日历 → 时段 → 确认 → 提交
   */
  async bookNewFromIdentity(id: { passport?: string, hkid?: string, license?: string }, prefs: UpdatePreferences): Promise<{ html: string, success: boolean, reference?: string, selectedDate?: string, selectedTimeslot?: string, error?: string }> {
    console.log('[TdabsFlows] bookNewFromIdentity 开始新建预约流程（完整流程）')

    // 保存 jobId 和 onProgress 回调
    this.jobId = prefs.jobId
    this.onProgress = prefs.onProgress

    // 记录开始（UI日志）
    await this.logProgress(5, 'startBooking', '开始预约')

    try {
      // 步骤1: 访问首页
      await this.logProgress(10, '访问首页')
      await this.navigateHome()

      // 步骤2: 同意条款并进入个人信息页
      await this.logProgress(20, '同意条款')
      const personalInfo = await this.agreeTermsAndEnterPersonalInfo()

      // 步骤3: 提交个人信息并验证身份
      await this.logProgress(35, '填写个人信息')
      const verifyResult = await this.submitPersonalInfoAndVerifyIdentity(
        personalInfo.html,
        id,
        {
          name: (prefs as any).licenseOwnerName || '',
          email: (prefs as any).email || '',
          phone: (prefs as any).phone || '',
          issuingCountry: (prefs as any).issuingCountry || 'CN',
          issuingAuthority: (prefs as any).issuingAuthority || 'GD',
        },
      )

      // 检查是否有错误
      const errorResult = this.parser.detectError(verifyResult.html)
      if (errorResult.success && errorResult.data) {
        const msg = String(errorResult.data.message || '')
        const code = String((errorResult.data as any)?.code || '').toUpperCase()
        const isRepeatLogin = code.includes('512-E-0044') || /重覆登入|重复登入|短時間內不停刷新|短时间内不停刷新/.test(msg)
        if (isRepeatLogin) {
          try { this.session.banCurrentProxy('512-E-0044') }
          catch {}
          console.log('[TdabsFlows] 命中 512-E-0044，当前代理已加入黑名单，快速失败')
          await this.logError(new Error('系统阻挡（512-E-0044）'), '身份验证')
          return { html: verifyResult.html, success: false, error: '系统阻挡（512-E-0044）' }
        }
        console.log('[TdabsFlows] bookNewFromIdentity 身份验证失败:', msg)
        await this.logError(new Error(msg || '身份验证失败'), '身份验证')
        return { html: verifyResult.html, success: false, error: msg || '身份验证失败' }
      }

      await this.logProgress(50, '身份验证')
      console.log('[TdabsFlows] bookNewFromIdentity 身份验证完成，开始获取日历')

      // 步骤4: 处理可能的电话补填或事务处选择页
      let currentHtml = verifyResult.html
      let currentUrl = verifyResult.url

      // 检查是否需要填写电话号码（分支处理）
      // 从HTML：电话补填页的onClickContinue() -> action = cmdCheckIdentifierAction，设置storedAppointmentPhoneNumber和inputType='N'
      // 电话号码格式要求：8位数字，以2、3、5、6、8、9开头（香港电话号码格式）
      if (currentHtml.includes('請輸入電話號碼') || currentHtml.includes('电话号码') || currentHtml.includes('txtPhoneNumber')) {
        console.log('[TdabsFlows] bookNewFromIdentity 需要填写电话号码')
        const phoneFields = this.parser.extractFormFields(currentHtml, 'frmAppointment')

        // 处理电话号码格式：如果是中国大陆格式，转换为香港格式（使用默认值）
        // 或者使用提供的电话号码（如果格式正确）
        let phoneNumber = (prefs as any).phone || 'N/A'
        if (phoneNumber !== 'N/A' && phoneNumber) {
          // 验证是否符合香港电话号码格式（8位数字，以2、3、5、6、8、9开头）
          const hkPhoneRegex = /^[235689]\d{7}$/
          if (!hkPhoneRegex.test(phoneNumber)) {
            console.log('[TdabsFlows] bookNewFromIdentity 电话号码格式不符合香港格式，使用默认值')
            phoneNumber = '21234567' // 使用默认的有效格式
          }
        }
        else {
          phoneNumber = '21234567' // 使用默认的有效格式
        }

        const phoneForm = {
          ...(phoneFields.success && phoneFields.data ? phoneFields.data : {}),
          ...this.baseForm({
            storedAppointmentOperation: 'A', // 继续新建预约流程
            inputType: 'N', // onClickContinue()中设置
          }),
          storedAppointmentPhoneNumber: phoneNumber,
        }
        // 电话补填页的onClickContinue()提交到cmdCheckIdentifierAction（与个人信息页的提交目标一致）
        const phoneRes = await this.session.postForm(
          this.url('cmd=cmdCheckIdentifierAction'),
          phoneForm,
          currentUrl,
        )
        currentHtml = phoneRes.body
        currentUrl = phoneRes.url
        await this.saveHtml('booking_04b_phone_input', currentHtml)
      }

      // 检查是否需要选择事务处（分支处理）
      // 从HTML：事务处选择页的onClickContinue() -> action = cmdGetCalendarAction，设置storedAppointmentOfficeCode
      const needOffice = currentHtml.includes('officeSelect') || currentHtml.includes('listLicenseCenter')
      if (needOffice) {
        console.log('[TdabsFlows] bookNewFromIdentity 需要选择事务处')
        const officeCode = prefs.officeCode || 'HKLO'
        const officeFields = this.parser.extractFormFields(currentHtml, 'frmAppointment')
        const officeForm = {
          ...(officeFields.success && officeFields.data ? officeFields.data : {}),
          ...this.baseForm({
            storedAppointmentOperation: 'A', // 继续新建预约流程
          }),
          storedAppointmentOfficeCode: officeCode, // onClickContinue()设置：选中officeSelect的值
        }
        // 事务处选择页的onClickContinue()直接提交到cmdGetCalendarAction（直接进入日历页）
        const officeRes = await this.session.postForm(
          this.url('cmd=cmdGetCalendarAction'),
          officeForm,
          currentUrl,
        )
        currentHtml = officeRes.body
        currentUrl = officeRes.url
        await this.saveHtml('booking_04c_office_select', currentHtml)
      }

      // 步骤5: 获取日历（如果还没有在日历页）
      // 检查是否已经在日历页（通过URL或页面内容判断，需要更严格的检查）
      // 真正的日历页应该有：日历表头、日期链接或日历表格结构
      const hasCalendarHeader = currentHtml.includes('calendar-table-header-cell')
      const hasDateLinks = currentHtml.includes('onClickDate')
      const hasCalendarTable = currentHtml.includes('calendar-table') || currentHtml.includes('calendar-inner-table')
      const isAlreadyCalendar = currentUrl.includes('cmdGetCalendarAction')
        && (hasCalendarHeader || (hasDateLinks && hasCalendarTable))

      let resCalendar: HttpResponse
      if (!isAlreadyCalendar) {
        // 如果不在日历页，提交表单获取日历
        const form1Fields = this.parser.extractFormFields(currentHtml, 'frmAppointment')
        const form1 = {
          ...(form1Fields.success && form1Fields.data ? form1Fields.data : {}),
          ...this.baseForm({ storedAppointmentOperation: 'A' }), // 继续新建预约流程
        }
        resCalendar = await this.session.postForm(
          this.url('cmd=cmdGetCalendarAction'),
          form1,
          currentUrl,
        )
        await this.saveHtml('booking_05_calendar', resCalendar.body)
      }
      else {
        // 已经在日历页，直接使用当前HTML
        console.log('[TdabsFlows] bookNewFromIdentity 已在日历页，跳过获取日历步骤')
        resCalendar = { body: currentHtml, url: currentUrl } as HttpResponse
      }

      // 解析日历统计信息（即使没有可用日期也要打印）
      const calSummary = this.analyzeCalendarSummary(resCalendar.body)
      if (calSummary) {
        console.log(`📆 ${calSummary.year}年${calSummary.monthIndex0 + 1}月日历解析完成: 总计 ${calSummary.daysInMonth} 天, 可预约 ${calSummary.availableCount} 天, 休息日 ${calSummary.restCount} 天, 约满 ${calSummary.bookedCount} 天, 暂不可约 ${calSummary.tempCount} 天`)
      }

      const datesResult: ParseResult<string[]> = this.parser.parseAvailableDates(resCalendar.body)

      // 过滤70天内的日期（放号时间段限制）
      const today = new Date()
      today.setHours(0, 0, 0, 0)
      const maxDate = new Date(today)
      maxDate.setDate(today.getDate() + 70)
      maxDate.setHours(23, 59, 59, 999)

      // 检查是否强制使用偏好日期
      const forcePreferred = !!(prefs.forcePreferredDates)

      // 解析允许日期范围（用户配置的限制）
      const parseAllowed = (s?: string): Date | undefined => {
        if (!s || !String(s).trim())
          return undefined
        const v = String(s).trim()
        if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
          const [y, m, d] = v.split('-').map(Number)
          const dt = new Date(y, m - 1, d)
          dt.setHours(0, 0, 0, 0)
          return dt
        }
        try {
          const [dd, mm, yyyy] = v.split('-').map(Number)
          const dt = new Date(yyyy, (mm || 1) - 1, dd)
          dt.setHours(0, 0, 0, 0)
          return dt
        }
        catch { return undefined }
      }
      // 优先级：若预约数据设置"强制使用首选日期"，则不应用 allowed 范围，仅命中首选才继续
      const allowedStart = forcePreferred ? undefined : parseAllowed(prefs.allowedDateRange?.start)
      const allowedEnd = forcePreferred ? undefined : parseAllowed(prefs.allowedDateRange?.end)

      // 确定实际的日期范围：优先使用 allowedDateRange，否则使用 70 天窗口
      const effectiveStart = allowedStart || today
      const effectiveEnd = allowedEnd || maxDate

      // 如果指定了 allowedDateRange，打印日志
      if (!forcePreferred && (allowedStart || allowedEnd)) {
        console.log(`[TdabsFlows] 📅 用户配置的允许日期范围: ${allowedStart ? allowedStart.toISOString().split('T')[0] : '无限制'} ~ ${allowedEnd ? allowedEnd.toISOString().split('T')[0] : '无限制'}`)
      }
      if (forcePreferred && prefs.preferredDates && prefs.preferredDates.length > 0) {
        console.log(`[TdabsFlows] 🔒 强制使用偏好日期模式已启用，必须命中以下日期之一: ${prefs.preferredDates.join(', ')}`)
      }

      // DEBUG：强制成功路径（直接选择窗口内首日，但继续流程）
      let forceSelectedDate: string | null = null
      if (this.isDebugForceSuccess()) {
        const pickedD = String(effectiveStart.getDate()).padStart(2, '0')
        const pickedM = String(effectiveStart.getMonth() + 1).padStart(2, '0')
        const pickedY = effectiveStart.getFullYear()
        forceSelectedDate = `${pickedD}-${pickedM}-${pickedY}`
        console.log(`[TdabsFlows] ⚠️ DEBUG_FORCE_SUCCESS: 直接选择伪造可用日期: ${forceSelectedDate}`)
        // 不返回，继续流程到时段选择
      }

      // 处理当前月的日期
      let availableDates = datesResult.success && datesResult.data ? datesResult.data : []
      // 统一的日期过滤函数：同时检查 70 天窗口、allowedDateRange 和周末
      const inBaseWindow = (dateStr: string): boolean => {
        try {
          const dt = parseDDMMYYYY(dateStr)
          // 排除周六日
          const dow = dt.getDay()
          if (dow === 0 || dow === 6)
            return false
          // 必须在 70 天窗口内
          if (dt.getTime() < today.getTime() || dt.getTime() > maxDate.getTime())
            return false
          // 如果指定了 allowedDateRange，必须在该范围内
          if (allowedStart && dt.getTime() < allowedStart.getTime())
            return false
          if (allowedEnd && dt.getTime() > allowedEnd.getTime())
            return false
          return true
        }
        catch {
          return false
        }
      }
      availableDates = availableDates.filter(inBaseWindow)

      // 合并调试注入的模拟日期（DEBUG_MONITOR_MOCK_DATES 和 DEBUG_MONITOR_RANDOM_RELEASE）
      let effectiveAvailableDates = Array.isArray(availableDates) ? availableDates.slice() : []
      const addedMockDates: string[] = []
      try {
        const nodeEnv = String(process.env.NODE_ENV || '').toLowerCase()
        const debugEnabled = nodeEnv !== 'production'
        const randomReleaseEnv = String(process.env.DEBUG_MONITOR_RANDOM_RELEASE || '').trim()
        const useRandomRelease = debugEnabled && randomReleaseEnv === '1'
        const mockDatesEnv = String(process.env.DEBUG_MONITOR_MOCK_DATES || '').trim()

        // 调试日志：打印环境变量状态
        console.log(`[TdabsFlows] 🐛 调试环境检查: NODE_ENV=${nodeEnv || '(未设置)'}, DEBUG_MONITOR_RANDOM_RELEASE=${randomReleaseEnv || '(未设置)'}, DEBUG_MONITOR_MOCK_DATES=${mockDatesEnv || '(未设置)'}, debugEnabled=${debugEnabled}`)

        let debugMockDates: string[] = []
        if (debugEnabled) {
          if (mockDatesEnv)
            debugMockDates = mockDatesEnv.split(/[;,\s]+/).map(s => s.trim()).filter(Boolean)
          // 兼容 YYYY-MM-DD 与 DD-MM-YYYY，两者统一转换为 DD-MM-YYYY
          if (debugMockDates.length > 0) {
            const toDMY = (v: string) => {
              const s = String(v || '').trim()
              const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
              return m ? `${m[3]}-${m[2]}-${m[1]}` : s
            }
            debugMockDates = debugMockDates.map(toDMY)
          }
        }
        // 合并 mock dates（仅当前月且在基础窗口内）
        if (debugEnabled && debugMockDates.length > 0) {
          const set = new Set<string>(effectiveAvailableDates)
          for (const d of debugMockDates) {
            try {
              const dt = parseDDMMYYYY(d)
              if (dt.getFullYear() === today.getFullYear() && dt.getMonth() === today.getMonth() && inBaseWindow(d)) {
                if (!set.has(d))
                  addedMockDates.push(d)
                set.add(d)
              }
            }
            catch {}
          }
          effectiveAvailableDates = Array.from(set)
          if (addedMockDates.length > 0)
            console.log(`[TdabsFlows] 🧪 调试: 注入模拟日期（Mock）`, { 日期: addedMockDates })
        }
        // 合并随机放号（覆盖当前月份）
        // 注意：如果指定了 allowedDateRange，只有当月份与允许范围有交集时才会生成随机放号
        if (useRandomRelease) {
          // 检查当前月份是否与 allowedDateRange 有交集
          const currentMonthStart = new Date(today.getFullYear(), today.getMonth(), 1)
          const currentMonthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0)
          currentMonthEnd.setHours(23, 59, 59, 999)

          const hasOverlap = !allowedStart
            || !allowedEnd
            || (allowedStart <= currentMonthEnd && allowedEnd >= currentMonthStart)

          if (!hasOverlap) {
            console.log(`[TdabsFlows] ⚠️ 调试: 跳过当前月随机放号（当前月 ${today.getFullYear()}年${today.getMonth() + 1}月 不在允许日期范围内）`)
          }
          else {
            console.log(`[TdabsFlows] 🧪 调试: 启用随机放号，生成模拟日期...`)
            // 确定生成范围：如果指定了 allowedDateRange，使用 allowedDateRange，否则使用 70 天窗口
            const genStartDate = allowedStart || today
            const genEndDate = allowedEnd || maxDate

            // 确保生成范围在当前月份内（与当前月求交集）
            const actualStartDate = genStartDate > currentMonthStart ? genStartDate : currentMonthStart
            const actualEndDate = genEndDate < currentMonthEnd ? genEndDate : currentMonthEnd

            const gen = generateRandomRelease({
              startDate: actualStartDate,
              endDate: actualEndDate,
              year: today.getFullYear(),
              month0: today.getMonth(),
            })

            // 进一步过滤：只保留在允许范围内的日期
            const filteredGenDates = gen.availableDates.filter(inBaseWindow)
            const set = new Set<string>(effectiveAvailableDates)
            for (const d of filteredGenDates)
              set.add(d)
            effectiveAvailableDates = Array.from(set)
            if (filteredGenDates.length > 0)
              console.log(`[TdabsFlows] 🧪 调试: 随机放号（Random）`, { 日期: filteredGenDates, 配额: Object.fromEntries(filteredGenDates.map(d => [d, gen.quotas[d] || 1])) })
            else
              console.log(`[TdabsFlows] ⚠️ 调试: 随机放号未生成任何日期（当前月在允许范围内但没有符合条件的日期）`)
          }
        }
        else if (debugEnabled && randomReleaseEnv !== '1') {
          console.log(`[TdabsFlows] ⚠️ 调试: DEBUG_MONITOR_RANDOM_RELEASE 未设置为 '1'，跳过随机放号`)
        }
      }
      catch (e) {
        console.error(`[TdabsFlows] ❌ 调试注入错误:`, e)
      }
      availableDates = effectiveAvailableDates

      // 如果当前月没有可用日期，扫描后续月份（最多70天内）
      // 注意：强制偏好日期模式下的跨月扫描在后面的逻辑中单独处理
      if (availableDates.length === 0 && !forcePreferred) {
        const scanRangeText = (allowedStart || allowedEnd)
          ? `允许日期范围（${allowedStart ? allowedStart.toISOString().split('T')[0] : '无限制'} ~ ${allowedEnd ? allowedEnd.toISOString().split('T')[0] : '无限制'}）`
          : '70天内'
        console.log(`[TdabsFlows] bookNewFromIdentity 当前月没有可用日期，扫描后续月份（${scanRangeText}）`)
        const calendarFields = this.parser.extractFormFields(resCalendar.body, 'frmAppointment')
        let currentFields = calendarFields.success && calendarFields.data ? calendarFields.data : {}

        // 确定要扫描的月份列表
        // 如果指定了 allowedDateRange，优先扫描该范围内的月份
        const monthsToScan: Array<{ year: number, month0: number }> = []

        if (allowedStart || allowedEnd) {
          // 如果指定了允许范围，只扫描该范围内的月份
          const startMonthDate = new Date(effectiveStart)
          startMonthDate.setDate(1)
          const endMonthDate = new Date(effectiveEnd)
          endMonthDate.setDate(1)

          // 使用年份和月份索引来遍历，避免修改 Date 对象
          let currentYear = startMonthDate.getFullYear()
          let currentMonth0 = startMonthDate.getMonth()
          const endYear = endMonthDate.getFullYear()
          const endMonth0 = endMonthDate.getMonth()

          while (currentYear < endYear || (currentYear === endYear && currentMonth0 <= endMonth0)) {
            const monthFirstDay = new Date(currentYear, currentMonth0, 1)
            monthFirstDay.setHours(0, 0, 0, 0)
            const maxDateMidnight = new Date(maxDate)
            maxDateMidnight.setHours(23, 59, 59, 999)

            // 仍然需要确保在 70 天窗口内
            if (monthFirstDay <= maxDateMidnight) {
              monthsToScan.push({
                year: currentYear,
                month0: currentMonth0,
              })
            }

            // 移动到下一个月
            currentMonth0++
            if (currentMonth0 > 11) {
              currentMonth0 = 0
              currentYear++
            }
          }
        }
        else {
          // 如果没有指定允许范围，按顺序扫描后续月份（最多3个月）
          let scanYear = today.getFullYear()
          let scanMonth0 = today.getMonth()
          const maxMonths = 3

          for (let i = 0; i < maxMonths; i++) {
            scanMonth0++
            if (scanMonth0 > 11) {
              scanMonth0 = 0
              scanYear++
            }

            const monthFirstDay = new Date(scanYear, scanMonth0, 1)
            monthFirstDay.setHours(0, 0, 0, 0)
            const maxDateMidnight = new Date(maxDate)
            maxDateMidnight.setHours(23, 59, 59, 999)

            if (monthFirstDay > maxDateMidnight)
              break

            monthsToScan.push({ year: scanYear, month0: scanMonth0 })
          }
        }

        for (const { year: scanYear, month0: scanMonth0 } of monthsToScan) {
          console.log(`[TdabsFlows] bookNewFromIdentity 扫描 ${scanYear}年${scanMonth0 + 1}月`)
          const monthCalendarRes = await this.session.postForm(
            this.url('cmd=cmdGetCalendarAction'),
            {
              ...currentFields,
              ...this.baseForm({ storedAppointmentOperation: 'A' }),
              storedAppointmentCalendarYear: scanYear.toString(),
              storedAppointmentCalendarMonth: scanMonth0.toString(),
            },
            this.url('cmd=cmdGetCalendarAction'),
          )

          // 解析并打印该月的日历统计信息
          const monthCalSummary = this.analyzeCalendarSummary(monthCalendarRes.body)
          if (monthCalSummary) {
            console.log(`📆 ${monthCalSummary.year}年${monthCalSummary.monthIndex0 + 1}月日历解析完成: 总计 ${monthCalSummary.daysInMonth} 天, 可预约 ${monthCalSummary.availableCount} 天, 休息日 ${monthCalSummary.restCount} 天, 约满 ${monthCalSummary.bookedCount} 天, 暂不可约 ${monthCalSummary.tempCount} 天`)
          }

          const monthDates = this.parser.parseAvailableDates(monthCalendarRes.body)
          if (monthDates.success && monthDates.data) {
            let monthFilteredDates = monthDates.data.filter(inBaseWindow)

            // 在扫描后续月份时也应用调试注入
            try {
              const nodeEnv = String(process.env.NODE_ENV || '').toLowerCase()
              const debugEnabled = nodeEnv !== 'production'
              const useRandomRelease = debugEnabled && String(process.env.DEBUG_MONITOR_RANDOM_RELEASE || '').trim() === '1'
              let debugMockDates: string[] = []
              if (debugEnabled) {
                const envMock = String(process.env.DEBUG_MONITOR_MOCK_DATES || '').trim()
                if (envMock)
                  debugMockDates = envMock.split(/[;,\s]+/).map(s => s.trim()).filter(Boolean)
                if (debugMockDates.length > 0) {
                  const toDMY = (v: string) => {
                    const s = String(v || '').trim()
                    const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
                    return m ? `${m[3]}-${m[2]}-${m[1]}` : s
                  }
                  debugMockDates = debugMockDates.map(toDMY)
                }
              }
              // 合并 mock dates（仅当前扫描月份且在基础窗口内）
              if (debugEnabled && debugMockDates.length > 0) {
                const set = new Set<string>(monthFilteredDates)
                for (const d of debugMockDates) {
                  try {
                    const dt = parseDDMMYYYY(d)
                    if (dt.getFullYear() === scanYear && dt.getMonth() === scanMonth0 && inBaseWindow(d)) {
                      set.add(d)
                    }
                  }
                  catch {}
                }
                monthFilteredDates = Array.from(set)
              }
              // 合并随机放号（覆盖当前扫描月份）
              if (useRandomRelease) {
                // 检查扫描月份是否与 allowedDateRange 有交集
                const scanMonthStart = new Date(scanYear, scanMonth0, 1)
                const scanMonthEnd = new Date(scanYear, scanMonth0 + 1, 0)
                scanMonthEnd.setHours(23, 59, 59, 999)

                const hasOverlap = !allowedStart
                  || !allowedEnd
                  || (allowedStart <= scanMonthEnd && allowedEnd >= scanMonthStart)

                if (!hasOverlap) {
                  console.log(`[TdabsFlows] ⚠️ 调试: 跳过 ${scanYear}年${scanMonth0 + 1}月随机放号（该月不在允许日期范围内）`)
                }
                else {
                  // 确定生成范围：如果指定了 allowedDateRange，使用 allowedDateRange，否则使用 70 天窗口
                  const genStartDate = allowedStart || today
                  const genEndDate = allowedEnd || maxDate

                  // 确保生成范围在扫描月份内（与扫描月求交集）
                  const actualStartDate = genStartDate > scanMonthStart ? genStartDate : scanMonthStart
                  const actualEndDate = genEndDate < scanMonthEnd ? genEndDate : scanMonthEnd

                  const gen = generateRandomRelease({
                    startDate: actualStartDate,
                    endDate: actualEndDate,
                    year: scanYear,
                    month0: scanMonth0,
                  })

                  // 进一步过滤：只保留在允许范围内的日期
                  const filteredGenDates = gen.availableDates.filter(inBaseWindow)
                  const set = new Set<string>(monthFilteredDates)
                  for (const d of filteredGenDates)
                    set.add(d)
                  monthFilteredDates = Array.from(set)
                  if (filteredGenDates.length > 0)
                    console.log(`[TdabsFlows] 🧪 调试: 随机放号（Random）`, { 月: `${scanYear}-${scanMonth0 + 1}`, 日期: filteredGenDates })
                  else
                    console.log(`[TdabsFlows] ⚠️ 调试: ${scanYear}年${scanMonth0 + 1}月随机放号未生成任何日期（该月在允许范围内但没有符合条件的日期）`)
                }
              }
            }
            catch {
              // 忽略调试注入错误
            }

            availableDates.push(...monthFilteredDates)

            if (monthFilteredDates.length > 0) {
              console.log(`[TdabsFlows] bookNewFromIdentity 在 ${scanYear}年${scanMonth0 + 1}月 找到 ${monthFilteredDates.length} 个可用日期`)
              break
            }

            // 更新表单字段
            const monthFields = this.parser.extractFormFields(monthCalendarRes.body, 'frmAppointment')
            if (monthFields.success && monthFields.data) {
              currentFields = { ...currentFields, ...monthFields.data }
            }
          }
        }
      }

      // 如果启用了强制成功模式，使用强制日期
      if (forceSelectedDate) {
        availableDates = [forceSelectedDate]
      }

      // 强制偏好日期模式下，即使当前月没有可用日期，也要继续尝试跨月查找偏好日期
      if (availableDates.length === 0 && !forcePreferred) {
        console.log('[TdabsFlows] bookNewFromIdentity 70天内无可用日期')
        await this.logError(new Error('70天内无可用日期'), '查询可用日期')
        return { html: resCalendar.body, success: false, error: '70天内无可用日期' }
      }
      // 强制偏好日期模式下，如果当前月没有可用日期，会在后续逻辑中尝试跨月查找
      if (availableDates.length === 0 && forcePreferred) {
        console.log('[TdabsFlows] 🔒 强制模式：当前月无可用日期，将尝试跨月查找偏好日期')
      }
      if (availableDates.length > 0) {
        console.log('[TdabsFlows] bookNewFromIdentity 找到可用日期', availableDates.length, '个（70天内）')
        await this.logProgress(65, '查询可用日期', `找到 ${availableDates.length} 个可用日期`)
      }

      // 兼容 YYYY-MM-DD 与 DD-MM-YYYY
      const toDMY2 = (v: string) => {
        const s = String(v || '').trim()
        if (/^\d{2}-\d{2}-\d{4}$/.test(s))
          return s
        const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
        if (m)
          return `${m[3]}-${m[2]}-${m[1]}`
        return s
      }

      let selectedDate: string | null = null

      // 检查偏好日期
      if (prefs.preferredDates && prefs.preferredDates.length > 0) {
        const prefDMY = (prefs.preferredDates as string[]).map(toDMY2)
        const hit = prefDMY.find(d => availableDates.includes(d))
        if (hit) {
          selectedDate = hit
          console.log(`[TdabsFlows] ✅ 找到偏好日期: ${selectedDate}`)
        }
        else if (forcePreferred) {
          // 强制模式：如果当前月没有命中偏好日期，需要跨月扫描
          console.log(`[TdabsFlows] 🔒 强制模式：当前月未命中偏好日期，将扫描后续月份查找偏好日期`)
        }
      }

      // 如果强制模式且未命中偏好日期，继续扫描后续月份
      if (forcePreferred && !selectedDate && prefs.preferredDates && prefs.preferredDates.length > 0) {
        const prefDMY = (prefs.preferredDates as string[]).map(toDMY2)
        const calendarFields = this.parser.extractFormFields(resCalendar.body, 'frmAppointment')
        let currentFields = calendarFields.success && calendarFields.data ? calendarFields.data : {}

        // 确定要扫描的月份（基于偏好日期）
        const prefDatesParsed = prefDMY.map((d) => {
          try {
            return parseDDMMYYYY(d)
          }
          catch {
            return null
          }
        }).filter((d): d is Date => d !== null).sort((a, b) => a.getTime() - b.getTime())

        if (prefDatesParsed.length > 0) {
          const firstPrefDate = prefDatesParsed[0]
          const lastPrefDate = prefDatesParsed[prefDatesParsed.length - 1]
          const monthsToScan: Array<{ year: number, month0: number }> = []

          let currentYear = firstPrefDate.getFullYear()
          let currentMonth0 = firstPrefDate.getMonth()
          const endYear = lastPrefDate.getFullYear()
          const endMonth0 = lastPrefDate.getMonth()

          while (currentYear < endYear || (currentYear === endYear && currentMonth0 <= endMonth0)) {
            const monthFirstDay = new Date(currentYear, currentMonth0, 1)
            monthFirstDay.setHours(0, 0, 0, 0)
            const maxDateMidnight = new Date(maxDate)
            maxDateMidnight.setHours(23, 59, 59, 999)

            // 确保在 70 天窗口内
            if (monthFirstDay <= maxDateMidnight) {
              monthsToScan.push({
                year: currentYear,
                month0: currentMonth0,
              })
            }

            // 移动到下一个月
            currentMonth0++
            if (currentMonth0 > 11) {
              currentMonth0 = 0
              currentYear++
            }
          }

          // 遍历月份查找偏好日期
          for (const { year, month0 } of monthsToScan) {
            if (selectedDate)
              break

            // 检查是否需要切换月份（不是当前月份才需要切换）
            const currentCalendarYear = this.parser.extractFormFields(resCalendar.body, 'frmAppointment').data?.storedAppointmentCalendarYear
            const currentCalendarMonth = this.parser.extractFormFields(resCalendar.body, 'frmAppointment').data?.storedAppointmentCalendarMonth
            const needSwitchMonth = !currentCalendarYear || !currentCalendarMonth || Number(currentCalendarYear) !== year || Number(currentCalendarMonth) !== (month0 + 1)

            if (needSwitchMonth) {
              const formForMonth = {
                ...currentFields,
                ...this.baseForm({
                  storedAppointmentOperation: 'A',
                  storedAppointmentCalendarYear: String(year),
                  storedAppointmentCalendarMonth: String(month0 + 1),
                }),
              }
              const monthCalendarRes = await this.session.postForm(
                this.url('cmd=cmdGetCalendarAction'),
                formForMonth,
                this.url('cmd=cmdGetCalendarAction'),
              )
              await this.saveHtml(`booking_05_calendar_${year}-${month0 + 1}`, monthCalendarRes.body)

              // 更新表单字段
              const monthFields = this.parser.extractFormFields(monthCalendarRes.body, 'frmAppointment')
              if (monthFields.success && monthFields.data) {
                currentFields = { ...currentFields, ...monthFields.data }
              }

              // 解析该月的可用日期
              const monthDatesResult: ParseResult<string[]> = this.parser.parseAvailableDates(monthCalendarRes.body)
              const monthAvailableDates = monthDatesResult.success && monthDatesResult.data ? monthDatesResult.data : []
              const monthFilteredDates = monthAvailableDates.filter(inBaseWindow)

              // 检查是否有偏好日期
              const hit = prefDMY.find(d => monthFilteredDates.includes(d))
              if (hit) {
                selectedDate = hit
                // 更新 availableDates 和 resCalendar 以便后续使用
                availableDates = [hit]
                resCalendar = monthCalendarRes
                console.log(`[TdabsFlows] ✅ 在 ${year}年${month0 + 1}月找到偏好日期: ${selectedDate}`)
                break
              }
            }
            else {
              // 当前月份，直接在已有的 availableDates 中查找
              const hit = prefDMY.find(d => availableDates.includes(d))
              if (hit) {
                selectedDate = hit
                console.log(`[TdabsFlows] ✅ 在当前月找到偏好日期: ${selectedDate}`)
                break
              }
            }
          }

          // 强制模式：如果最终未命中偏好日期，返回错误
          if (!selectedDate) {
            console.log(`[TdabsFlows] ❌ 强制模式：未命中偏好日期（${prefs.preferredDates.join(', ')}）`)
            await this.logError(new Error(`未命中偏好日期（强制）: ${prefs.preferredDates.join(', ')}`), '查询可用日期')
            return { html: resCalendar.body, success: false, error: `未命中偏好日期（强制）: ${prefs.preferredDates.join(', ')}` }
          }
        }
        else {
          // 偏好日期格式无效
          console.log(`[TdabsFlows] ❌ 强制模式：偏好日期格式无效（${prefs.preferredDates.join(', ')}）`)
          await this.logError(new Error(`偏好日期格式无效: ${prefs.preferredDates.join(', ')}`), '查询可用日期')
          return { html: resCalendar.body, success: false, error: `偏好日期格式无效: ${prefs.preferredDates.join(', ')}` }
        }
      }

      // 非强制模式：如果未命中偏好日期，选择第一个可用日期
      if (!selectedDate) {
        selectedDate = availableDates[0]
        console.log(`[TdabsFlows] ℹ️ 未命中偏好日期，选择第一个可用日期: ${selectedDate}`)
      }

      // 步骤6: 获取时段
      const form2Fields = this.parser.extractFormFields(resCalendar.body, 'frmAppointment')
      const form2 = {
        ...(form2Fields.success && form2Fields.data ? form2Fields.data : {}),
        ...this.baseForm({
          storedAppointmentOperation: 'A', // 继续新建预约流程
          storedAppointmentTimeslotDate: selectedDate,
        }),
      }
      const resTimeslot = await this.session.postForm(
        this.url('cmd=cmdGetTimeslotListAction'),
        form2,
        this.url('cmd=cmdGetCalendarAction'),
      )
      await this.saveHtml('booking_06_timeslot', resTimeslot.body)
      await this.logProgress(80, '选择时段')

      /**
       * 生成模拟的时段ID（使用真实格式：6-7位数字，类似真实系统）
       * 真实格式示例：1590813（纯数字）
       */
      const generateMockTimeslotId = (startTime: string, endTime: string): string => {
        // 基于时间和随机数生成一个看起来真实的时段ID
        // 使用时间段作为种子，生成一个6-7位数字
        const timeHash = (startTime + endTime).split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
        const random = Math.floor(Math.random() * 900000) + 100000 // 6位随机数
        const id = (timeHash * 1000 + random) % 10000000 // 确保是7位数字以内
        return String(Math.max(100000, Math.min(9999999, id))) // 限制在6-7位
      }

      // DEBUG：强制成功路径（构造伪造时段）
      let selected: TimeslotInfo | null = null
      if (this.isDebugForceSuccess()) {
        const mockId = generateMockTimeslotId('1000', '1100')
        selected = {
          id: mockId,
          display_time: '10:00-11:00',
          start_time: '1000',
          end_time: '1100',
        } as TimeslotInfo
        console.log(`[TdabsFlows] ⚠️ DEBUG_FORCE_SUCCESS: 直接选择模拟时段: ${selected.display_time} (ID: ${mockId})`)
      }
      else {
        // 检查是否启用模拟时段（DEBUG_REQUEST_MOCK_TIMESLOTS）
        const nodeEnv = String(process.env.NODE_ENV || '').toLowerCase()
        const debugEnabled = nodeEnv !== 'production'
        const mockTimeslotsEnv = debugEnabled ? String(process.env.DEBUG_REQUEST_MOCK_TIMESLOTS || '').trim() : ''
        const useMockTimeslots = debugEnabled && mockTimeslotsEnv !== '' && mockTimeslotsEnv !== '0'

        // 调试日志：打印时段模拟环境变量状态
        console.log(`[TdabsFlows] 🐛 时段模拟环境检查: DEBUG_REQUEST_MOCK_TIMESLOTS=${mockTimeslotsEnv || '(未设置或为空)'}, useMockTimeslots=${useMockTimeslots}`)

        if (useMockTimeslots) {
          // 从环境变量读取模拟时段，格式：
          // - '1' 或空：使用默认 '10:00-11:00'
          // - '10:00-11:00'：单个时段
          // - '10:00-11:00;14:00-15:00'：多个时段（使用第一个）
          const mockTimeslots = mockTimeslotsEnv === '1' ? ['10:00-11:00'] : mockTimeslotsEnv.split(';').filter(Boolean)
          if (mockTimeslots.length > 0) {
            const firstSlot = mockTimeslots[0]
            // 解析格式：10:00-11:00 或 1000-1100
            let startTime = ''
            let endTime = ''
            let displayTime = firstSlot
            if (firstSlot.includes('-')) {
              const [start, end] = firstSlot.split('-')
              if (start.includes(':')) {
                // HH:MM 格式
                displayTime = `${start}-${end}`
                startTime = start.replace(':', '')
                endTime = end.replace(':', '')
              }
              else {
                // HHMM 格式
                startTime = start
                endTime = end
                displayTime = `${start.slice(0, 2)}:${start.slice(2)}-${end.slice(0, 2)}:${end.slice(2)}`
              }
            }
            const mockId = generateMockTimeslotId(startTime, endTime)
            selected = {
              id: mockId,
              display_time: displayTime,
              start_time: startTime,
              end_time: endTime,
            } as TimeslotInfo
            console.log(`[TdabsFlows] 🧪 调试: 注入模拟时段`, { 时段: selected.display_time, ID: mockId })
          }
        }
        else {
          // 正常解析时段
          const tResult = this.parser.parseAvailableTimeslots(resTimeslot.body)
          if (!tResult.success || !tResult.data || tResult.data.length === 0) {
            console.log('[TdabsFlows] bookNewFromIdentity 无可用时段')
            await this.logError(new Error('无可用时段'), '选择时段')
            if (debugEnabled) {
              console.log(`[TdabsFlows] 💡 提示: 如需启用模拟时段，请在 .env 文件中设置 DEBUG_REQUEST_MOCK_TIMESLOTS=1 或 DEBUG_REQUEST_MOCK_TIMESLOTS="10:00-11:00"`)
            }
            return { html: resTimeslot.body, success: false, error: '无可用时段' }
          }
          console.log('[TdabsFlows] bookNewFromIdentity 找到可用时段', tResult.data.length, '个')
          selected = tResult.data[0]
        }
      }

      if (!selected) {
        console.log('[TdabsFlows] bookNewFromIdentity 无可用时段')
        await this.logError(new Error('无可用时段'), '选择时段')
        return { html: resTimeslot.body, success: false, error: '无可用时段' }
      }

      // 如果是正常解析的时段，且用户有首选时段偏好，则匹配首选
      if (!this.isDebugForceSuccess() && String(process.env.DEBUG_REQUEST_MOCK_TIMESLOTS || '').trim() !== '1') {
        const tResult = this.parser.parseAvailableTimeslots(resTimeslot.body)
        if (tResult.success && tResult.data && tResult.data.length > 0 && prefs.preferredTimes && prefs.preferredTimes.length > 0) {
          for (const p of prefs.preferredTimes) {
            const found = tResult.data.find(s => s.display_time.includes(p) || s.start_time === p.replace(':', ''))
            if (found) {
              selected = found
              break
            }
          }
        }
      }

      // 步骤7: 确认预约
      const form3Fields = this.parser.extractFormFields(resTimeslot.body, 'frmAppointment')
      const form3 = {
        ...(form3Fields.success && form3Fields.data ? form3Fields.data : {}),
        ...this.baseForm({
          storedAppointmentOperation: 'A', // 继续新建预约流程
          storedAppointmentTimeslotId: selected.id,
          storedAppointmentTimeslotFromTime: selected.start_time,
          storedAppointmentTimeslotToTime: selected.end_time,
        }),
      }
      const resConfirm = await this.session.postForm(
        this.url('cmd=cmdConfirmAppointment'),
        form3,
        this.url('cmd=cmdGetTimeslotListAction'),
      )
      await this.saveHtml('booking_07_confirm', resConfirm.body)
      await this.logProgress(95, '确认预约')

      // DEBUG：强制成功路径（仅在明确设置了 DEBUG_BOOKING_FORCE_SUCCESS=1 时跳过）
      if (this.isDebugForceSuccess()) {
        const ref = `FAKE-${Date.now().toString().slice(-6)}`
        console.log(`[TdabsFlows] ⚠️ DEBUG_FORCE_SUCCESS: 直接模拟预约成功（跳过实际提交）`, {
          参考编号: ref,
          日期: selectedDate,
          时间: selected?.display_time,
        })
        return {
          html: resConfirm.body,
          success: true,
          reference: ref,
          selectedDate: selectedDate || undefined,
          selectedTimeslot: selected?.display_time || undefined,
        }
      }

      // 步骤8: 提交预约（记录完整的请求信息用于分析）
      const finalFields = this.parser.extractFormFields(resConfirm.body, 'frmAppointment')
      const form4 = {
        ...(finalFields.success && finalFields.data ? finalFields.data : {}),
        ...this.baseForm({ storedAppointmentOperation: 'A' }), // 继续新建预约流程
      }

      // 确保关键字段存在（参考真实请求）
      // storedAppointmentDLNewIssue 在文档中的真实请求中是 "N"，如果缺失则添加
      if (!('storedAppointmentDLNewIssue' in form4) || form4.storedAppointmentDLNewIssue === '') {
        form4.storedAppointmentDLNewIssue = 'N'
      }

      // 记录提交请求的详细信息（用于分析）
      const submitUrl = this.url('cmd=cmdSubmitAppointmentAction')
      const submitReferer = this.url('cmd=cmdConfirmAppointment')
      console.log(`[TdabsFlows] 📤 准备提交预约请求`)
      console.log(`[TdabsFlows] 📤 提交URL: ${submitUrl}`)
      console.log(`[TdabsFlows] 📤 Referer: ${submitReferer}`)
      console.log(`[TdabsFlows] 📤 提交表单数据:`, JSON.stringify(form4, null, 2))

      // 保存请求信息到文件（包含完整请求头信息）
      try {
        // 提取 Origin（用于记录）
        let originHeader = ''
        try {
          const refererUrl = new URL(submitReferer)
          originHeader = `${refererUrl.protocol}//${refererUrl.host}`
        }
        catch {}

        const requestInfo = {
          timestamp: new Date().toISOString(),
          url: submitUrl,
          referer: submitReferer,
          origin: originHeader,
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Referer': submitReferer,
            'Origin': originHeader || undefined,
          },
          formData: form4,
          formDataEncoded: Object.entries(form4)
            .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
            .join('&'),
          // 对比分析：标注与真实请求的差异
          comparison: {
            realRequestHas: {
              storedAppointmentTimeslotId: '数字格式（如：1590813）',
              storedAppointmentDLNewIssue: 'N',
              Origin: 'https://abs1.td.gov.hk',
              additionalHeaders: ['Cache-Control: max-age=0', 'Sec-Fetch-* headers'],
            },
            currentRequestHas: {
              storedAppointmentTimeslotId: form4.storedAppointmentTimeslotId || '(缺失)',
              storedAppointmentDLNewIssue: form4.storedAppointmentDLNewIssue || '(缺失)',
              Origin: originHeader || '(缺失)',
            },
          },
        }
        const requestInfoPath = await this.saveRequestInfo('booking_08_submit_request', JSON.stringify(requestInfo, null, 2))
        console.log(`[TdabsFlows] 📤 请求信息已保存: ${requestInfoPath}`)
      }
      catch (error) {
        console.warn(`[TdabsFlows] ⚠️ 保存请求信息失败:`, error)
      }

      const resSubmit = await this.session.postForm(
        submitUrl,
        form4,
        submitReferer,
      )
      await this.saveHtml('booking_08_submit_result', resSubmit.body)

      const booking = this.parser.parseBookingResult(resSubmit.body)
      if (booking.success && booking.data?.success) {
        const refNumber = booking.data.reference_number
        const apptDate = selectedDate || undefined
        const apptTime = selected?.display_time || undefined

        console.log('[TdabsFlows] bookNewFromIdentity 预约成功，参考号:', refNumber)

        // 记录完整的预约成功信息到文件日志（便于修复脚本提取）
        try {
          const logger = getLogger()
          logger.info('预约成功', {
            message: '预约流程成功',
            referenceNumber: refNumber,
            参考编号: refNumber,
            日期: apptDate,
            时间: apptTime,
            selectedDate: apptDate,
            selectedTimeslot: apptTime,
            category: 'booking',
            component: 'TdabsFlows',
          })
        }
        catch (e) {
          // 日志记录失败不应影响主流程
          console.warn('[TdabsFlows] 记录预约成功日志失败:', e)
        }

        await this.logProgress(100, 'complete', '预约完成')
        return {
          html: resSubmit.body,
          success: true,
          reference: refNumber,
          selectedDate: apptDate, // 实际选择的日期
          selectedTimeslot: apptTime, // 实际选择的时段
        }
      }

      // 解析错误信息（可能来自 detectError 检测到的业务错误）
      let errorMessage = '提交失败'
      if (booking.error) {
        errorMessage = booking.error
      }
      else if (booking.success && booking.data) {
        // booking.data 存在但 success 为 false，说明是业务错误（如时段已满）
        errorMessage = booking.data.message || booking.data.error_code || '提交失败'
        if (booking.data.error_code) {
          console.log(`[TdabsFlows] bookNewFromIdentity 预约失败: ${booking.data.error_code} - ${errorMessage}`)
        }
        else {
          console.log(`[TdabsFlows] bookNewFromIdentity 预约失败: ${errorMessage}`)
        }
      }
      else {
        console.log('[TdabsFlows] bookNewFromIdentity 预约失败: 无法解析预约结果')
      }

      await this.logError(new Error(errorMessage), '提交预约')
      return { html: resSubmit.body, success: false, error: errorMessage }
    }
    catch (error) {
      const errorMsg = error instanceof Error ? error.message : String(error)
      console.error('[TdabsFlows] bookNewFromIdentity 执行出错:', errorMsg)
      try {
        // 命中网络/SSL/TLS/连接类错误 → 临时拉黑当前代理，便于下次换出口
        const m = String(errorMsg || '').toLowerCase()
        const isNetErr = /econnreset|econnrefused|etimedout|enotfound|enetunreach|timeout|request\s+timeout|socket\s+hang\s+up|client\s+network\s+socket\s+disconnected.*tls|err_ssl|ssl\s+protocol|wrong\s+version/.test(m)
        if (isNetErr) {
          try { this.session.banCurrentProxy('network_error') }
          catch {}
        }
      }
      catch {}
      await this.logError(error instanceof Error ? error : new Error(errorMsg), '预约流程')
      return { html: '', success: false, error: `预约流程执行失败: ${errorMsg}` }
    }
  }

  /**
   * DEBUG：是否启用强制成功（用于无可用日期环境下的联调/测试）。
   * 开关：环境变量 DEBUG_BOOKING_FORCE_SUCCESS=1
   */
  private isDebugForceSuccess(): boolean {
    try {
      return process.env.DEBUG_BOOKING_FORCE_SUCCESS === '1'
    }
    catch {
      return false
    }
  }
}
